package org.xiaoxin.onecoupon.engine.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import jodd.util.StringUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendResult;
import org.checkerframework.checker.i18nformatter.qual.I18nFormat;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.PathResource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.xiaoxin.onecoupon.engine.common.constant.EngineRedisConstant;
import org.xiaoxin.onecoupon.engine.common.context.UserContext;
import org.xiaoxin.onecoupon.engine.common.enums.RedisStockDecrementErrorEnum;
import org.xiaoxin.onecoupon.engine.common.enums.UserCouponStatusEnum;
import org.xiaoxin.onecoupon.engine.dao.entity.CouponSettlementDO;
import org.xiaoxin.onecoupon.engine.dao.entity.UserCouponDO;
import org.xiaoxin.onecoupon.engine.dao.mapper.CouponSettlementMapper;
import org.xiaoxin.onecoupon.engine.dao.mapper.CouponTemplateMapper;
import org.xiaoxin.onecoupon.engine.dao.mapper.UserCouponMapper;
import org.xiaoxin.onecoupon.engine.dto.req.*;
import org.xiaoxin.onecoupon.engine.dto.resp.CouponTemplateQueryRespDTO;
import org.xiaoxin.onecoupon.engine.mq.event.UserCouponRedeemEvent;
import org.xiaoxin.onecoupon.engine.mq.producer.UserConponRedeemProducer;
import org.xiaoxin.onecoupon.engine.mq.producer.UserCouponDelayCloseProducer;
import org.xiaoxin.onecoupon.engine.service.CouponTemplateService;
import org.xiaoxin.onecoupon.engine.service.UserConponService;
import org.xiaoxin.onecoupon.engine.toolkit.StockDecrementReturnCombinedUtil;
import org.xiaoxin.onecoupon.framework.exception.ClientException;
import org.xiaoxin.onecoupon.framework.exception.ServiceException;
import org.xiaoxin.onecoupon.engine.mq.event.UserCouponDelayCloseEvent;

import java.math.BigDecimal;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;

import static org.xiaoxin.onecoupon.engine.common.constant.EngineRedisConstant.USER_COUPON_TEMPLATE_LIST_KEY;

/**
 * @author xiaoxin
 * 2025/5/12
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class UserConponServiceImpl implements UserConponService {
   private final CouponTemplateService couponTemplateServicel;
   private final static String STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH = "lua/stock_decrement_and_save_user_receive.lua";
   private  final StringRedisTemplate stringRedisTemplate;
   private  final TransactionTemplate transactionTemplate;
   private  final CouponTemplateMapper couponTemplateMapper;
   private  final UserCouponMapper userCouponMapper;
   private  final UserCouponDelayCloseProducer couponDelayCloseProducer;
   private  final UserConponRedeemProducer userConponRedeemProducer;

   private  final RedissonClient redissonClient;
   private  final CouponTemplateService couponTemplateService;
   private  final CouponSettlementMapper couponSettlementMapper;


   @Value("${one-coupon.user-coupon-list.save-cache.type:direct}")
   private String userCouponListSaveCacheType;
   @Override
   public void redeemUserConpon(CouponTemplateRedeemReqDTO requestParam) {
      //1.验证优惠劵是否存在，优惠劵是否在有效时间内
      CouponTemplateQueryRespDTO couponTemplate = couponTemplateServicel.findCouponTemplate(BeanUtil.toBean(requestParam, CouponTemplateQueryReqDTO.class));
      boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
      if (!isInTime){
         // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
         throw new ClientException("不满足优惠券领取时间");
      }
      //2.扣减Redis中优惠劵模板库存
      //2.1获取lua脚本
      DefaultRedisScript<Long> buildLuaRedisScript = Singleton.get(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH, () -> {
         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)));
         redisScript.setResultType(Long.class);
         return redisScript;
      });
      JSONObject receiveRule = JSONObject.parseObject(couponTemplate.getReceiveRule());
      String limitPerPerson = receiveRule.getString("limitPerPerson");
      //2.1执行lua脚本
      String couponTemplateCacheKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId());
      String userConponTemplateLimitCacheKey = String.format(EngineRedisConstant.USER_COUPON_TEMPLATE_LIMIT_KEY, UserContext.getUserId(), requestParam.getCouponTemplateId());
      Long stockDecrementLuaResult = stringRedisTemplate.execute(
              buildLuaRedisScript,
              ListUtil.of(couponTemplateCacheKey, userConponTemplateLimitCacheKey),
              String.valueOf(couponTemplate.getValidEndTime().getTime()), limitPerPerson
      );
      //脚本执行结果
      //请求是否成功：有 3 个参数，0 代表请求成功，1 代表优惠券已被领取完啦，2 代表用户已经达到领取上限。
      //用户领取次数：初始化为 0，每次领取成功后自增加 1。
      //2.2 判断 LUA 脚本执行返回类，如果失败根据类型返回报错提示
      long firstField = StockDecrementReturnCombinedUtil.extractFirstField(stockDecrementLuaResult);
      if (RedisStockDecrementErrorEnum.isFail(firstField)){
         throw new ServiceException(RedisStockDecrementErrorEnum.fromType(firstField));
      }
      //3.保存优惠劵领取记录到数据库和Redis中
      // 通过编程式事务执行优惠券库存自减以及增加用户优惠券领取记录
      long secondField = StockDecrementReturnCombinedUtil.extractSecondField(stockDecrementLuaResult);
      transactionTemplate.executeWithoutResult(status -> {
         try{
            //3.1优惠券库存自减
            int decremented=couponTemplateMapper.decrementCouponTemplateStock(Long.parseLong(requestParam.getShopNumber()),Long.parseLong(requestParam.getCouponTemplateId()),1L);
            if (!SqlHelper.retBool(decremented)){
               throw  new ServiceException("优惠劵已被领取完毕");
            }
            //3.2 添加 用户领取的优惠券记录列表
            Date now = new Date();
            DateTime validEndTime = DateUtil.offsetHour(now, JSONObject.parseObject(couponTemplate.getConsumeRule()).getInteger("validityPeriod"));
            UserCouponDO userCouponDO = UserCouponDO.builder()
                    .couponTemplateId(Long.parseLong(requestParam.getCouponTemplateId()))
                    .userId(Long.parseLong(UserContext.getUserId()))
                    .source(requestParam.getSource())
                    .receiveTime(now)
                    .validStartTime(now)
                    .validEndTime(validEndTime)
                    .receiveCount(Long.valueOf(secondField).intValue())
                    .status(UserCouponStatusEnum.UNUSED.getCode())
                    .build();
            userCouponMapper.insert(userCouponDO);
            // 保存优惠券缓存集合有两个选项：direct 在流程里直接操作，binlog 通过解析数据库日志后操作
            if (StrUtil.equals(userCouponListSaveCacheType, "direct")){
               //3.3 将用户已领取的优惠券添加到 Redis 缓存中。
               String userConponListCacheKey = String.format(USER_COUPON_TEMPLATE_LIST_KEY, UserContext.getUserId());
               String userConponItemCacheKey = StrUtil.builder()
                       .append(requestParam.getCouponTemplateId())
                       .append("_")
                       .append(userCouponDO.getId())
                       .toString();
               stringRedisTemplate.opsForZSet().add(userConponListCacheKey,userConponItemCacheKey,now.getTime());
               // 由于 Redis 在持久化或主从复制的极端情况下可能会出现数据丢失，而我们对指令丢失几乎无法容忍，因此我们采用经典的写后查询策略来应对这一问题
               Double scored;
               try {
                  scored = stringRedisTemplate.opsForZSet().score(userConponListCacheKey, userConponItemCacheKey);
                  // scored 为空意味着可能 Redis Cluster 主从同步丢失了数据，比如 Redis 主节点还没有同步到从节点就宕机了，解决方案就是再新增一次
                  if (scored == null){
                     stringRedisTemplate.opsForZSet().add(userConponListCacheKey,userConponItemCacheKey,now.getTime());
                     // 如果这里也新增失败了怎么办？我们大概率做不到绝对的万无一失，只能尽可能增加成功率
                  }
               }catch (Throwable ex){
                  log.warn("查询Redis用户优惠券记录为空或抛异常，可能Redis宕机或主从复制数据丢失，基础错误信息：{}", ex.getMessage());
                  // 如果直接抛异常大概率 Redis 宕机了，所以应该写个延时队列向 Redis 重试放入值。为了避免代码复杂性，这里直接写新增，大家知道最优解决方案即可
                  stringRedisTemplate.opsForZSet().add(userConponListCacheKey,userConponItemCacheKey,now.getTime());
               }
               // 发送延时消息队列，等待优惠券到期后，将优惠券信息从缓存中删除
               UserCouponDelayCloseEvent userCouponDelayCloseEvent = UserCouponDelayCloseEvent.builder()
                       .couponTemplateId(requestParam.getCouponTemplateId())
                       .userCouponId(String.valueOf(userCouponDO.getId()))
                       .delayTime(validEndTime.getTime())
                       .userId(UserContext.getUserId())
                       .build();
               SendResult sendResult = couponDelayCloseProducer.sendMessage(userCouponDelayCloseEvent);
               // 发送消息失败解决方案简单且高效的逻辑之一：打印日志并报警，通过日志搜集并重新投递
               if (ObjectUtil.notEqual(sendResult.getSendStatus().name(), "SEND_OK")) {
                  log.warn("发送优惠券关闭延时队列失败，消息参数：{}", JSON.toJSONString(userCouponDelayCloseEvent));
               }
            }

         }catch (Exception ex){
            status.setRollbackOnly();
            // 优惠券已被领取完业务异常
            if (ex instanceof ServiceException) {
               throw (ServiceException) ex;
            }
            if (ex instanceof DuplicateKeyException) {
               log.error("用户重复领取优惠券，用户ID：{}，优惠券模板ID：{}", UserContext.getUserId(), requestParam.getCouponTemplateId());
               throw new ServiceException("用户重复领取优惠券");
            }
            throw new ServiceException("优惠券领取异常，请稍候再试");
         }
      });

   }

   @Override
   public void redeemUserCouponByMQ(CouponTemplateRedeemReqDTO requestParam) {
      //1.验证优惠劵是否存在，优惠劵是否在有效时间内
      CouponTemplateQueryRespDTO couponTemplate = couponTemplateServicel.findCouponTemplate(BeanUtil.toBean(requestParam, CouponTemplateQueryReqDTO.class));
      boolean isInTime = DateUtil.isIn(new Date(), couponTemplate.getValidStartTime(), couponTemplate.getValidEndTime());
      if (!isInTime){
         // 一般来说优惠券领取时间不到的时候，前端不会放开调用请求，可以理解这是用户调用接口在“攻击”
         throw new ClientException("不满足优惠券领取时间");
      }
      //2.扣减Redis中优惠劵模板库存
      //2.1获取lua脚本
      DefaultRedisScript<Long> buildLuaRedisScript = Singleton.get(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH, () -> {
         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
         redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(STOCK_DECREMENT_AND_SAVE_USER_RECEIVE_LUA_PATH)));
         redisScript.setResultType(Long.class);
         return redisScript;
      });
      JSONObject receiveRule = JSONObject.parseObject(couponTemplate.getReceiveRule());
      String limitPerPerson = receiveRule.getString("limitPerPerson");
      //2.1执行lua脚本
      String couponTemplateCacheKey = String.format(EngineRedisConstant.COUPON_TEMPLATE_KEY, requestParam.getCouponTemplateId());
      String userConponTemplateLimitCacheKey = String.format(EngineRedisConstant.USER_COUPON_TEMPLATE_LIMIT_KEY, UserContext.getUserId(), requestParam.getCouponTemplateId());
      Long stockDecrementLuaResult = stringRedisTemplate.execute(
              buildLuaRedisScript,
              ListUtil.of(couponTemplateCacheKey, userConponTemplateLimitCacheKey),
              String.valueOf(couponTemplate.getValidEndTime().getTime()), limitPerPerson
      );
      //脚本执行结果
      //请求是否成功：有 3 个参数，0 代表请求成功，1 代表优惠券已被领取完啦，2 代表用户已经达到领取上限。
      //用户领取次数：初始化为 0，每次领取成功后自增加 1。
      //2.2 判断 LUA 脚本执行返回类，如果失败根据类型返回报错提示
      long firstField = StockDecrementReturnCombinedUtil.extractFirstField(stockDecrementLuaResult);
      if (RedisStockDecrementErrorEnum.isFail(firstField)){
         throw new ServiceException(RedisStockDecrementErrorEnum.fromType(firstField));
      }
      UserCouponRedeemEvent userCouponRedeemEvent = UserCouponRedeemEvent.builder()
              .requestParam(requestParam)
              .couponTemplate(couponTemplate)
              .receiveCount((int) StockDecrementReturnCombinedUtil.extractSecondField(stockDecrementLuaResult))
              .userId(UserContext.getUserId())
              .build();
      SendResult sendResult = userConponRedeemProducer.sendMessage(userCouponRedeemEvent);
      // 发送消息失败解决方案简单且高效的逻辑之一：打印日志并报警，通过日志搜集并重新投递
      if (ObjectUtil.notEqual(sendResult.getSendStatus().name(), "SEND_OK")) {
         log.warn("发送优惠券兑换消息失败，消息参数：{}", JSON.toJSONString(userCouponRedeemEvent));
      }


   }

   @Override
   public void createPaymentRecord(CouponCreatePaymentReqDTO requestParam) {
      //1.获取分布式锁
      RLock lock = redissonClient.getLock(String.format(EngineRedisConstant.LOCK_COUPON_SETTLEMENT_KEY, requestParam.getCouponId()));
      boolean tryLock = lock.tryLock();
      if (!tryLock){
         throw new ClientException("正在创建优惠券结算单，请稍候再试");
      }
      try {
         //2.查看当前优惠劵是否有结算纪录
         LambdaQueryWrapper<CouponSettlementDO> queryWrapper = Wrappers.lambdaQuery(CouponSettlementDO.class)
                 .eq(CouponSettlementDO::getCouponId, requestParam.getCouponId())
                 .eq(CouponSettlementDO::getUserId, Long.parseLong(UserContext.getUserId()))
                 .in(CouponSettlementDO::getStatus, 0, 2);
         if(couponSettlementMapper.selectOne(queryWrapper)!=null){
            throw new ClientException("请检查优惠券是否已使用");
         }
         //3.检查用户所使用优惠劵的状态
         LambdaQueryWrapper<UserCouponDO> queryWrapper1 = Wrappers.lambdaQuery(UserCouponDO.class)
                 .eq(UserCouponDO::getId, requestParam.getCouponId())
                 .eq(UserCouponDO::getUserId, Long.parseLong(UserContext.getUserId()));
         UserCouponDO userCouponDO = userCouponMapper.selectOne(queryWrapper1);
         if (ObjectUtil.isNull(userCouponDO)){
            throw  new ClientException("优惠劵不存在");
         }
         if (userCouponDO.getValidEndTime().before(new Date())){
            throw  new ClientException("优惠劵已经失效");
         }
         if (userCouponDO.getStatus()!=0){
            throw  new ClientException("优惠劵使用状态异常");
         }
         //4.获取优惠劵模板和消费规则
         CouponTemplateQueryRespDTO couponTemplate = couponTemplateService.findCouponTemplate(new CouponTemplateQueryReqDTO(requestParam.getShopNumber(), String.valueOf(userCouponDO.getCouponTemplateId())));
         JSONObject consumRule = JSONObject.parseObject(couponTemplate.getConsumeRule());
         //5.算折扣金额
         BigDecimal discountAmount;
         //5.1 商品专属优惠劵
         if(couponTemplate.getTarget().equals(0)){
            //获取第一个匹配商品
            //Optional 是 Java 8 引入的一个容器类，用于表示一个值可能存在或不存在的情况，从而避免 NullPointerException。
            Optional<CouponCreatePaymentGoodsReqDTO> matchedGoods = requestParam.getGoodsList().stream()
                    .filter(each -> Objects.equals(couponTemplate.getGoods(), each.getGoodsNumber()))
                    .findFirst();
            if (matchedGoods.isEmpty()){
               throw  new ClientException("商品信息与优惠劵模板信息不一致");
            }
            CouponCreatePaymentGoodsReqDTO paymentGoods = matchedGoods.get();
            BigDecimal maximumDiscountAmount = consumRule.getBigDecimal("maximumDiscountAmount");
            if (!paymentGoods.getGoodsAmount().subtract(maximumDiscountAmount).equals(paymentGoods.getGoodsPayableAmount())){
               throw new ClientException("商品折扣后金额异常");
            }
            discountAmount=maximumDiscountAmount;
         }else{
            //5.2 全店通用优惠劵
            //检查店铺编号
            if (!requestParam.getShopNumber().equals(couponTemplate.getShopNumber())&&couponTemplate.getSource().equals(0)){
               throw new ClientException("店铺编号不一致");
            }
            //检查订单金额是否满足最低消费金额
            BigDecimal termsOfUse = consumRule.getBigDecimal("termsOfUse");
            if (requestParam.getOrderAmount().compareTo(termsOfUse)<0){
               throw  new ClientException("订单金额不满足优惠劵消费条件");
            }
            //判断优惠劵类型
            BigDecimal maximumDiscountAmount = consumRule.getBigDecimal("maximumDiscountAmount");
            switch (couponTemplate.getType()){
               case 0: //立减券
                  discountAmount=maximumDiscountAmount;
                  break;
               case 1: //满减券
                  discountAmount=maximumDiscountAmount;
                  break;
               case 2: //折扣券
                  BigDecimal discountRate = consumRule.getBigDecimal("discountRate");
                  discountAmount = requestParam.getOrderAmount().multiply(discountRate);
                  if (discountAmount.compareTo(termsOfUse)>=0){
                     discountAmount=maximumDiscountAmount;
                  }
                  break;
               default:
                  throw new ClientException("无效的优惠券类型");
            }

         }
         // 6.计算折扣后金额并进行检查
         BigDecimal actualPayableAmount = requestParam.getOrderAmount().subtract(discountAmount);
         if (actualPayableAmount.compareTo(requestParam.getPayableAmount()) != 0) {
            throw new ClientException("折扣后金额不一致");
         }
         // 7.通过编程式事务减小事务范围
         transactionTemplate.executeWithoutResult(status->{
            try {
               //创建优惠劵结算单记录
               CouponSettlementDO couponSettlementDO = CouponSettlementDO.builder()
                       .couponId(requestParam.getCouponId())
                       .createTime(new Date())
                       .userId(Long.parseLong(UserContext.getUserId()))
                       .status(0)
                       .orderId(requestParam.getOrderId())
                       .build();
               couponSettlementMapper.insert(couponSettlementDO);
               //变更用户优惠劵状态
               LambdaUpdateWrapper<UserCouponDO> updateWrapper = Wrappers.lambdaUpdate(UserCouponDO.class)
                       .eq(UserCouponDO::getId, requestParam.getCouponId())
                       .eq(UserCouponDO::getUserId, Long.parseLong(UserContext.getUserId()))
                       .eq(UserCouponDO::getStatus, UserCouponStatusEnum.UNUSED.getCode());
               UserCouponDO updateUserCouponDo = UserCouponDO.builder()
                       .status(UserCouponStatusEnum.LOCKING.getCode()).build();
               userCouponMapper.update(updateUserCouponDo,updateWrapper);
            }catch (Exception ex){
               log.error("创建优惠券结算单失败", ex);
               status.setRollbackOnly();
               throw ex;
            }
         });
         // 8.从用户可用优惠券列表中删除优惠券
         String userCouponItemCacheKey = StrUtil.builder()
                 .append(userCouponDO.getCouponTemplateId())
                 .append("_")
                 .append(userCouponDO.getId())
                 .toString();
         stringRedisTemplate.opsForZSet().remove(String.format(USER_COUPON_TEMPLATE_LIST_KEY, UserContext.getUserId()), userCouponItemCacheKey);

      }finally {
         lock.unlock();
      }



   }

   @Override
   public void processPayment(CouponProcessPaymentReqDTO  requestParam) {
      //1.获取分布式锁
      RLock lock = redissonClient.getLock(String.format(EngineRedisConstant.LOCK_COUPON_SETTLEMENT_KEY,requestParam.getCouponId()));
      boolean tryLock = lock.tryLock();
      if (!tryLock){
         throw  new ClientException("正在核销优惠劵结算单，稍后再试");
      }

      //通过编程式事务减小事务范围
      transactionTemplate.executeWithoutResult(status->{
         try {
            //2.修改优惠劵结算单
            LambdaUpdateWrapper<CouponSettlementDO> couponSettlementUpdateWrapper = Wrappers.lambdaUpdate(CouponSettlementDO.class)
                    .eq(CouponSettlementDO::getCouponId, requestParam.getCouponId())
                    .eq(CouponSettlementDO::getUserId, Long.parseLong(UserContext.getUserId()))
                    .eq(CouponSettlementDO::getStatus, 0);
            CouponSettlementDO couponSettlementDO = CouponSettlementDO.builder()
                    .status(2).build();
            int couponSettlementUpdated = couponSettlementMapper.update(couponSettlementDO, couponSettlementUpdateWrapper);
            if (!SqlHelper.retBool(couponSettlementUpdated)){
               log.error("核销优惠券结算单异常，请求参数：{}", com.alibaba.fastjson.JSON.toJSONString(requestParam));
               throw new ServiceException("核销优惠券结算单异常");
            }
            //3.更新用户优惠劵状态
            LambdaUpdateWrapper<UserCouponDO> userCouponUpdateWrapper = Wrappers.lambdaUpdate(UserCouponDO.class)
                    .eq(UserCouponDO::getId, requestParam.getCouponId())
                    .eq(UserCouponDO::getUserId, Long.parseLong(UserContext.getUserId()))
                    .eq(UserCouponDO::getStatus, UserCouponStatusEnum.LOCKING.getCode());
            UserCouponDO userCouponDO = UserCouponDO.builder()
                    .status(UserCouponStatusEnum.USED.getCode())
                    .build();
            int userCouponUpdated = userCouponMapper.update(userCouponDO, userCouponUpdateWrapper);
            if (!SqlHelper.retBool(userCouponUpdated)){
               log.error("修改用户优惠券记录状态已使用异常，请求参数：{}", com.alibaba.fastjson.JSON.toJSONString(requestParam));
               throw new ServiceException("修改用户优惠券记录状态异常");
            }
         }catch (Exception ex){
            log.error("核销优惠券结算单失败", ex);
            status.setRollbackOnly();
            throw ex;
         }finally {
            lock.unlock();
         }
      });

   }

   @Override
   public void processRefund(CouponProcessRefundReqDTO requestParam) {
      //1.获取分布式锁
      RLock lock = redissonClient.getLock(String.format(EngineRedisConstant.LOCK_COUPON_SETTLEMENT_KEY, requestParam.getCouponId()));
      boolean tryLock = lock.tryLock();
      if (!tryLock){
         throw  new ClientException("正在执行优惠券退款，请稍候再试");
      }

      transactionTemplate.executeWithoutResult(status->{
         try {
            //2.修改优惠劵结算单状态
            LambdaUpdateWrapper<CouponSettlementDO> couponSettlementUpdateWrapper = Wrappers.lambdaUpdate(CouponSettlementDO.class)
                    .eq(CouponSettlementDO::getCouponId, requestParam.getCouponId())
                    .eq(CouponSettlementDO::getUserId, Long.parseLong(UserContext.getUserId()))
                    .eq(CouponSettlementDO::getStatus, 2);
            CouponSettlementDO couponSettlementDO = CouponSettlementDO.builder()
                    .status(3)
                    .build();
            int conponSettlementUpdated = couponSettlementMapper.update(couponSettlementDO, couponSettlementUpdateWrapper);
            if (!SqlHelper.retBool(conponSettlementUpdated)){
               log.error("优惠券结算单退款异常，请求参数：{}", com.alibaba.fastjson.JSON.toJSONString(requestParam));
               throw new ServiceException("核销优惠券结算单异常");
            }
            //3.修改用户优惠劵状态
            LambdaUpdateWrapper<UserCouponDO> userCouponDOUpdateWrapper = Wrappers.lambdaUpdate(UserCouponDO.class)
                    .eq(UserCouponDO::getId, requestParam.getCouponId())
                    .eq(UserCouponDO::getUserId, Long.parseLong(UserContext.getUserId()))
                    .eq(UserCouponDO::getStatus, UserCouponStatusEnum.USED.getCode());
            UserCouponDO userCouponDO = UserCouponDO.builder()
                    .status(UserCouponStatusEnum.REVOKED.getCode())
                    .build();
            int userConponDOUpdated = userCouponMapper.update(userCouponDO, userCouponDOUpdateWrapper);
            if (!SqlHelper.retBool(userConponDOUpdated)){
               log.error("修改用户优惠券记录状态未使用异常，请求参数：{}", com.alibaba.fastjson.JSON.toJSONString(requestParam));
               throw new ServiceException("修改用户优惠券记录状态异常");
            }
         }catch (Exception ex){
            log.error("执行优惠券结算单退款失败", ex);
            status.setRollbackOnly();
            throw ex;
         }
      });
      //4.查询出来优惠券再放回缓存
      UserCouponDO userCouponDO = userCouponMapper.selectOne(Wrappers.lambdaQuery(UserCouponDO.class)
              .eq(UserCouponDO::getUserId, Long.parseLong(UserContext.getUserId()))
              .eq(UserCouponDO::getId, requestParam.getCouponId()));
      String userCouponItemCacheKey = StrUtil.builder()
              .append(userCouponDO.getCouponTemplateId())
              .append("_")
              .append(userCouponDO.getId())
              .toString();
      stringRedisTemplate.opsForZSet().add(String.format(USER_COUPON_TEMPLATE_LIST_KEY,userCouponDO.getId()),userCouponItemCacheKey,userCouponDO.getReceiveTime().getTime());
   }
}
